#######################################################
# The class of Simulation Engine using Simulatable models 
# Author: moonho.hwang@gmail.com
#   Date: Oct.19.2011
#######################################################
import threading
from Simulatable import *
from XSY.Base import *
import time
import sys 
import pkg_resources

def findCmdIndex(command, str):
   ''' '''
   for i in xrange(0, len(command)):
      if command[i] == str: return i
   return -1

class SimEngine(threading.Thread):
   '''The class of real-time execution of Simulatable model'''
   def __init__(self, sim=None, inject_CBF=None):
     '''consturctor '''
     threading.Thread.__init__(self)
     if isinstance(sim, Simulatable) == False:
        raise Exception("%s is not a Simulatable"%str(sim))
     else:
        self.model = sim
        self.inject_CBF = inject_CBF

        self.SkipLoop = True
        self.ToBeKilled = False
        #self.sim_thread = threading.Thread(self.loop()) 
        #self.Stop() # <----- don't try this.
        self.t_segment_start = self.t_segment_end = Clock.tNow=0
        self.XBuffer = list() # injection buffer
        self.CBuffer = list() # command buffer
        self.sfactor = 1 # to make maximized speed, set it float('inf') 
        self.StepByStep = False
        self.DisplayInterval = -1 # if it is negative, StepByStep works for DES

        self.simTimeBound = float('inf') # time bound of trajectories Apr/9/2012
        self.bVerbose = True # flag for tracing out or not Apr/12/2012
        self.iSimRun = 1 # current iteration of multiple simulation runs
        self.bAutoresetByTimebound = False # flag got automatic reset when run reach time bounds Apr/12/2012  
        self.TotalSimRuns = 1 # No Of Trajectories will be collected. Apr/12/2012
        self.TrajStatistics=[] # list of trajectory statistics 
  
   def init(self):
      '''make initialization of model and sim engine itself'''
      self.reset('')

   def setModel(self, sim):
      '''set the simulation model by sim.'''
      self.model = sim

   def compute_tNow(self):
      '''determine time now'''
      if self.sfactor == float('inf'): # then only next event simulation
         if self.model.timeNext() == float('inf') and self.simTimeBound == float('inf') :
            print "\nNext schedule reaches at infinity in the DES Only mode at time %.3f." % Clock.tNow
            self.pause()
         else:
            Clock.tNow = min(self.model.timeNext(), self.simTimeBound)
      else: # then hybrid continus + discrete event simulation
         t = self.t_segment_end + (time.clock()-self.t_segment_start)*self.sfactor
         Clock.tNow = min(min(self.model.timeNext(), t), self.simTimeBound)

      if Clock.tNow >= self.simTimeBound: # Apr/9/2012
         print "\nSimulation run time is bounded by %.3f.\n" % self.simTimeBound
         Clock.tNow = self.simTimeBound
         if self.bAutoresetByTimebound: # needs to be reset
            self.TrajStatistics.append(self.model.get_statistics_str())
            self.reset("")
            print 'Iter=%d'%self.iSimRun
            if self.iSimRun < self.TotalSimRuns: 
                self.iSimRun += 1
            else:
               print '############################################'
               print '   Multiple Simulation Rune is completed!'
               print '############################################'
               self.pause()
         else: # doesn't need to be reset
            self.pause()
            #if self.StepByStep and self.DisplayInterval>0: # Jul/21/2012
            if self.DisplayInterval>0: # Jul/21/2012
                self.simTimeBound += self.DisplayInterval

   def exp_handling(self):
      '''print exception handling'''
      print "Exception %s Occurs at time %.3f."%(sys.exc_info()[0], Clock.tNow)
      exception_handling() 
      self.pause()

   def print_src_and_event_segment(self, src_state_str, z, tnow):
      ''' print the source state and event segment '''
      print "\n%s\t at time %.3f."%(src_state_str, tnow) 
      zstr =""
      if not hasattr(z, '__iter__'): zstr = str(z)
      elif len(z)==1: 
         for e in z: zstr += "%s"%e  
      else:
         for e in z: zstr += "(%s)"%e  
      print "\t -- (%s,%.3f) -->"%(zstr, tnow)

   def run(self):
      ''' theading loop for simulation'''
      while self.ToBeKilled == False: 
         if self.SkipLoop == False:
            try: # begin the exception safe block
               DES = False
               if len(self.XBuffer)>0:
                  (target_md, ipv) = self.XBuffer.pop()
                  if self.bVerbose:
                     src_str, z, tnow = self.model.qstate_to_s(), ipv, Clock.tNow 
                     self.print_src_and_event_segment(src_str, z, tnow)
                  if target_md != None:
                     target_md.Delta_x(ipv, Clock.tNow); DES = True; 
                     if target_md != self.model and target_md.parent != None:
                        target_md.parent.update_tN(update_parent = True) # Apr/13/2012

               else: # then make time advance 
                  self.compute_tNow()
                  if Clock.tNow == self.model.timeNext():
                     if self.bVerbose:
                        src_str, tnow = self.model.qstate_to_s(), Clock.tNow 
                     z = self.model.Delta_y(Clock.tNow); DES = True; 
                     if self.bVerbose:
                        self.print_src_and_event_segment(src_str, z, tnow)

               if DES and self.StepByStep: #if transition is DES and StepByStep mode
                  self.pause()
               self.display_q()

            except:
               self.exp_handling() 
               raise

            time.sleep(.001)# higher precision looping
         else: 
            time.sleep(.01) # lower precision looping for SkipLoop
       #Thread.stop      #<==== don't try this 

 
   def reset(self, command):
      '''reset the associated model state'''
      self.t_segment_start = self.t_segment_end = Clock.tNow=0 #2012/7/4
      self.last_progress = 0

      if self.model != None:
        self.model.Delta_0(0) 

      del self.XBuffer[:] # clear all of list XBuffer
      del self.CBuffer[:] # clear all of command buffer
      if len(command)>1 and command[1] == "-a":
         self.TrajStatistics=[]
         self.iSimRun = 1 #

      # 7/25/2012 
      #if self.StepByStep and self.DisplayInterval>0: # animiation step by step
      if self.DisplayInterval>0: # animiation step by step
         self.simTimeBound = self.DisplayInterval # bound by DisplayInterval

   def make_run(self, command=""):
      '''make the simulation run'''
      if self.SkipLoop: # skipping
         self.t_segment_start = time.clock() # set the event segment start point 
         self.SkipLoop = False

   def display_modes(self):
      '''display mode settings'''
      print "=== (m)ode settings ==="
      print 'Current Simulation i = %d'%self.iSimRun
      print "StepByStep mode = %s, -step"%self.StepByStep
      print 'scale factor for speed =%s, -speed sf'%self.sfactor
      print "display time mode = %s, -dtm"%Clock.DisplayMode
      print 'Verbose mode = %s, -vb'%self.bVerbose
      print "simulation time bound = %.3f, -stb time"%self.simTimeBound
      print 'Autoreset mode by timebound = %s, -ar '%self.bAutoresetByTimebound
      print 'Total#ofSimRuns = %d, -tsr n'%self.TotalSimRuns
      print '\n'

   def changeMode(self, command):
      '''change modes of simulation'''

      if len(command)==1:
         self.display_modes()
         print 'change mode of simulation: autoreset, multiple_runs, speed, step, time, verbose'
         print 'use -step, -stepInv -speed sf, -dtm, -vb, -ar, -tsr no'
         return

      i = findCmdIndex(command, '-stb')
      if i > -1 and len(command) >= i+1:
         stb = command[i+1]
         result = True
         if stb == 'inf': self.simTimeBound = float('inf')
         elif Clock.tNow <= float(stb): self.simTimeBound = float(stb);
         else: print 'stb=%.3f is greater than tNow=%.3f'%(float(stb), Clock.tNow); result = False;

         if result: print 'new stb=%.3f'% self.simTimeBound

      i = findCmdIndex(command, '-ar')
      if i > -1:
         self.bAutoresetByTimebound = not self.bAutoresetByTimebound
         print 'The new Autoreset by timebound = %s.'%self.bAutoresetByTimebound
      i = findCmdIndex(command, '-tsr')
      if i > -1 and len(command) >= i+1: 
         no = int(command[i+1])
         if no > 0: 
            self.TotalSimRuns = no
            print 'Total#ofSimRuns = %d.'%self.TotalSimRuns
      i = findCmdIndex(command, '-speed')
      if i > -1 and len(command) >= i+1: 
         self.setSpeedFactor(command[i+1])

      i = findCmdIndex(command, '-step')
      if i > -1 : 
         self.toggleStepByStep()     

      i = findCmdIndex(command, '-dInterval') # 
      if i > -1 and len(command) >= i+1: 
         self.DisplayInterval = float(command[i+1])
         print 'DisplayInterval = %.3f.'%self.DisplayInterval

      i = findCmdIndex(command, '-dtm')
      if i > -1:
         self.setTimeDisplayMode()
      i = findCmdIndex(command, '-vb')
      if i > -1:
         self.bVerbose = not self.bVerbose 
         print 'New Verbose = %s'%self.bVerbose

   def pause(self):
      '''pause the loop'''
      if self.SkipLoop == False:
         self.t_segment_end = Clock.tNow
         # print "t_segment_end = %.3f\n"%self.t_segment_end # activate for debuggin

         self.model.Delta_null(Clock.tNow) # update all statistics 

         self.SkipLoop = True
         #if ds_menu: self.display_menu()
      pass

   def inject(self, JSONarg=None): # arg can be a JSON string
      '''inject an input event through a port'''
      #self.pause()
      if self.inject_CBF != None:
         try:
            (target_md, pvalue) = self.inject_CBF(self.model, JSONarg)
            if pvalue: # not None
               self.XBuffer.append((target_md, pvalue))
               self.make_run()
            else:
               print "None event generated!"

         except:
            self.exp_handling()
            raise
      else: 
         print "no callback function defined \n"
    
   def toggleStepByStep(self):
      '''toggle on/off of StepByStep mode'''
      if self.StepByStep: self.StepByStep = False
      else: self.StepByStep = True
      print "New StepByStep mode = %s"%self.StepByStep

   def terminate(self):
      '''terminate thread in which the thread joins to the main thread'''
      self.ToBeKilled = True
      #self.sim_thread.join
      self.join()

   def setSpeedFactor(self, inp):
      '''set speed factor that must be a positive real number or infinity.'''
      if inp == 'inf'or inp == "'inf'":
         self.sfactor = float('inf')
      else:
         f = float(inp)
         if f>0:
            self.sfactor = f
         else:
            print "speed fact MUST be a positive real number or inf"

      print "The new scale factor=%s."%self.sfactor

   def setTimeDisplayMode(self):
      '''set time display modes'''
      try:
         Clock.toggleDisplayMode()

      except:
	      self.exp_handling()
	      raise

   def display_menu(self):
      '''display interative menu''' 
      self.display_q()
      print '-- simulation menu --'
      print "e(x)it, (in)jet, (m)ode, (p)ause, reset, run, stat(i)stics>",

   def print_statistics(self, cmd):
      '''print out statistics of models'''
      if len(cmd)>1 and cmd[1] == "-a":
         for e in xrange(0, len(self.TrajStatistics)):
            i = int(e)
            print "Iter = %d\n %s"%(i+1,self.TrajStatistics[i])
            print "--------------------------------------------------------------- End of Iter = %d\n"%(i+1)
      else:
         print "======================================================================================\n"
         print "statistics of the current trajectory"
         print "%s"%(self.model.get_statistics_str())
         print "======================================================================================\n"

   def displayProgress(self):
      '''display progress bar'''
      progress = (Clock.tNow/self.simTimeBound)*100.0
      diff_progress = int(progress/10) - int(self.last_progress/10)
      self.last_progress = progress
      if diff_progress > 0 : 
            sys.stdout.write('.')

   def display_q(self):
      '''display current total state with time if bVerbose == True'''
      if self.bVerbose:
         print '-- the current state-- '
         print "%s\t at time %.3f."%(self.model.qstate_to_s(), Clock.tNow) 
      else:
         self.displayProgress()

   def display_help(self):
      '''display version of XSY'''
      print 'XSY version %s'%pkg_resources.get_distribution("XSY").version

   def execute_cmd(self, from_console):
      "execute command"
      sorg = ""
      if from_console:
         sorg = raw_input() 

      else: # pick a command from CBuffer until available
         while len(self.CBuffer) == 0:
             try: 
                time.sleep(.01) # wait until pick one
             except KeyboardInterrupt:
                self.terminate() # terminate thread
                print "Listening is interruped by Ctr+C!"
                raise KeyboardInterrupt

         sorg = self.CBuffer[0] # get the first command

      s = sorg.split() # split up the command by spaces
     
      if len(s)==0:
         return True
      if(s[0] == "run"):
         self.make_run(s)
      elif(s[0] == "reset"):
         self.reset(s)
      elif(s[0] == "statistics" or s[0] == "i"):
         self.print_statistics(s)
      elif(s[0] == "pause" or s[0] == "p"):
         self.pause()
      elif(s[0] == "in" or s[0] == "inject"):
         pos = len(s[0])
         self.inject(sorg[pos:]) # make JSON string 'space' non-sensitive
      elif(s[0] == "m" or s[0] == "mode"):
         self.changeMode(s)
      elif(s[0] == "exit" or s[0] == "x"):
         #self.terminate() # terminate thread
         return False
      elif len(s[0])>0:
         print "Invalid input = %s"%s[0];

      if from_console == False and len(self.CBuffer)>0: 
        self.CBuffer.pop() # remove the command now

      return True

   def runInteractiveMenu(self, from_console = True):
      '''run interativem menu through the console'''
      self.display_help()
      self.display_modes()
      self.init()  # 7/31/2012 
      self.start() # thread loop through self.run()
      self.display_q()
      Looping = True
      while Looping: 
         # print the initial state at time 0.0 
         try: 
            self.display_menu()
            Looping = self.execute_cmd(from_console)

#         try:    
#            s = raw_input().split()
#            Looping = self.execute_cmd(s)    
#
#         except:
#            self.exp_handling() 
#            self.terminate() # terminate thread
#            raise
         except KeyboardInterrupt:
            self.terminate() # terminate thread
            print "Simulation is interruped by Ctr+C!"
            sys.exit()
         
      self.terminate() # terminate thread
      print "Simluation is succesfully terminated!" 

if __name__ == '__main__':
   model = Simulatable("hmh")
   rtengine = SimEngine(model)
   rtengine.runInteractiveMenu()
